﻿using System.ComponentModel.Design;
using System.Diagnostics;
using System.IO;
using System.Security.AccessControl;

namespace BuildConfigGen
{
    internal class EnsureUpdateModeVerifier
    {
        // the goal of verification is to ensure the generation is up-to-date
        // if any changes would be made to output, verification should fail
        // check the contents of VerifyErrors for verification errors

        private bool verifyState;
        private readonly bool verifyFromConstructor;
        private List<string> VerifyErrors = new List<string>();
        internal Dictionary<string, string> CopiedFilesToCheck = new Dictionary<string, string>();
        internal Dictionary<string, string> RedirectedToTempl = new Dictionary<string, string>();
        private HashSet<string> tempsToKeep = new HashSet<string>();

        public EnsureUpdateModeVerifier(bool verifyOnly)
        {
            this.verifyState = verifyOnly;
            this.verifyFromConstructor = verifyOnly;
        }

        public IEnumerable<string> GetVerifyErrors(bool skipContentCheck)
        {
            foreach (var r in VerifyErrors)
            {
                yield return r;
            }

            if (!skipContentCheck)
            {
                foreach(var r in RedirectedToTempl)
                {
                    string? sourceFile;
                    string procesed = "";

                    string? tempToKeep = null;
                    sourceFile = r.Value;
                    procesed = $"(processed temp={sourceFile}) ";
                    tempToKeep = sourceFile;

                    string? contentError = null;
                    contentError = CompareFile(r, sourceFile, procesed, tempToKeep, contentError);

                    if (contentError is not null)
                    {
                        yield return contentError;
                    }
                }

                foreach (var r in CopiedFilesToCheck)
                {
                    string? sourceFile;
                    string procesed = "";

                    string? tempToKeep = null;
                    if (RedirectedToTempl.TryGetValue(r.Key, out sourceFile))
                    {
                        procesed = $"(processed temp={sourceFile}) ";
                        tempToKeep = sourceFile;
                    }
                    else
                    {
                        sourceFile = r.Value;
                    }

                    string? contentError = null;
                    contentError = CompareFile(r, sourceFile, procesed, tempToKeep, contentError);

                    if (contentError is not null)
                    {
                        yield return contentError;
                    }
                }
            }
        }

        private string? CompareFile(KeyValuePair<string, string> r, string sourceFile, string procesed, string? tempToKeep, string? contentError)
        {
            FileInfo fi = new FileInfo(r.Key);

            if (fi.Name.Equals("resources.resjson", StringComparison.OrdinalIgnoreCase)
                || fi.Name.Equals(".npmrc", StringComparison.OrdinalIgnoreCase))
            {
                // resources.resjson is generated by make.js and does not need to be verified
                // it can differ between configs if configs have different inputs (causes verifier to fail);

                // TODO: ignore .npmrc for now; it's known to be out-of-sync due to upstream updates
            }
            else
            {
                if (Helpers.FilesEqual(sourceFile, r.Key))
                {
                    // if overwrite and content match, everything is good!  Verification passed.
                }
                else
                {
                    if (tempToKeep != null)
                    {
                        this.tempsToKeep.Add(tempToKeep!);
                    }

                    contentError = $"Content doesn't match {r.Value} {procesed}to {r.Key} (overwrite=true).  Dest file doesn't match source.";
                }
            }

            return contentError;
        }

        public void CleanupTempFiles()
        {
            try
            {
                int count = 0;
                foreach (var f in RedirectedToTempl.Values)
                {
                    count++;
                    if (!tempsToKeep.Contains(f))
                    {
                        if (File.Exists(f))
                        {
                            File.Delete(f);
                        }
                    }
                }

                if (count > 0 && !verifyFromConstructor)
                {
                    throw new Exception("Expected RedirectedToTemp to be empty when !verifyFromConstructor");
                }
            }
            finally
            {
                this.VerifyErrors.Clear();
                this.RedirectedToTempl.Clear();
                this.CopiedFilesToCheck.Clear();
            }
        }

        internal void Copy(string sourceFileName, string destFileName, bool overwrite)
        {
            bool verifyOnly = UseVerifyOnlyForFile(destFileName);

            if (verifyOnly)
            {
                if (File.Exists(destFileName))
                {
                    if (overwrite)
                    {
                        // we might check the content here, but we defer it in cause the content gets updated in WriteAllText
                        string normalizedDestFileName = NormalizeFile(destFileName);
                        string normalizedSourceFileName = NormalizeFile(sourceFileName);
                        if (!CopiedFilesToCheck.TryAdd(normalizedDestFileName, normalizedSourceFileName))
                        {
                            CopiedFilesToCheck[normalizedDestFileName] = normalizedSourceFileName;
                        }
                    }
                    else
                    {
                        // similar exception we'd get if we invoked File.Copy with existing file and overwrite=false
                        throw new Exception($"destFileName={destFileName} exists and overwrite is false");
                    }
                }
                else
                {
                    VerifyErrors.Add($"Copy {sourceFileName} to {destFileName} (overwrite={overwrite}).  Dest file doesn't exist.");
                }
            }
            else
            {
                File.Copy(sourceFileName, destFileName, overwrite);
            }
        }

        internal void Move(string sourceFileName, string destFileName)
        {
            bool verifyOnlySource = UseVerifyOnlyForFile(sourceFileName);
            bool verifyOnlyDest = UseVerifyOnlyForFile(destFileName);

            if (verifyOnlySource != verifyOnlyDest)
            {
                throw new Exception($"BUG: both source and dest must be unconditional path or not sourceFileName={sourceFileName} destFileName={destFileName}");
            }

            var verifyOnly = verifyOnlySource || verifyOnlyDest;

            if (verifyOnly)
            {
                // verification won't pass if we encounter a move

                if (File.Exists(destFileName))
                {
                    // similar exception we'd get if we invoked File.Move with existing file
                    throw new Exception($"destFileName={destFileName} exists");
                }
                else
                {
                    VerifyErrors.Add($"Need to move {sourceFileName} to {destFileName}.  Dest file doesn't exist.");
                }
            }
            else
            {
                File.Move(sourceFileName, destFileName);
            }
        }

        internal void WriteAllText(string path, string contents, bool suppressValidationErrorIfTargetPathDoesntExist)
        {
            bool verifyOnly = UseVerifyOnlyForFile(path);

            if (verifyOnly)
            {
                if (File.Exists(path))
                {
                    string? tempFilePath;

                    string normalizedPath = NormalizeFile(path);

                    if (!RedirectedToTempl.TryGetValue(normalizedPath, out tempFilePath))
                    {
                        tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
                        RedirectedToTempl.Add(normalizedPath, tempFilePath);
                    }

                    //Console.WriteLine($"writing to tempFilePath={tempFilePath}");
                    File.WriteAllText(tempFilePath, contents);
                }
                else
                {
                    string item = $"Need to write content to {path} content.Length={contents.Length}, destination does not exist";

                    if (suppressValidationErrorIfTargetPathDoesntExist)
                    {
                        Console.WriteLine("Skipping adding validation warning due to suppressValidationErrorIfTargetPathDoesntExist: " + item);
                    }
                    else
                    {
                        VerifyErrors.Add(item);
                    }
                }
            }
            else
            {
                File.WriteAllText(path, contents);
            }
        }

        private string NormalizeFile(string file)
        {
            FileInfo fi = new FileInfo(file);
            return fi.FullName;
        }

        internal void DirectoryCreateDirectory(string path, bool suppressValidationErrorIfTargetPathDoesntExist)
        {
            bool verifyOnly = UseVerifyOnlyForPath(path);

            if (verifyOnly)
            {
                if (!Directory.Exists(path))
                {
                    string item = $"Need to create directory {path}";
                    if (suppressValidationErrorIfTargetPathDoesntExist)
                    {
                        Console.WriteLine("Skipping adding VerifyError due to suppressValidationErrorIfTargetPathDoesntExist=true:" + item);
                    }
                    else
                    {
                        VerifyErrors.Add(item);
                    }
                }
            }
            else
            {
                Directory.CreateDirectory(path);
            }
        }

        internal string FileReadAllText(string filePath)
        {
            bool verifyOnly = UseVerifyOnlyForFile(filePath);

            if (verifyOnly)
            {
                string targetFile = ResolveFile(filePath);

                return File.ReadAllText(targetFile);
            }
            else
            {
                return File.ReadAllText(filePath);
            }
        }

        internal string[] FileReadAllLines(string filePath)
        {
            bool verifyOnly = UseVerifyOnlyForFile(filePath);

            if (verifyOnly)
            {
                string targetFile = ResolveFile(filePath);

                return File.ReadAllLines(targetFile);
            }
            else
            {
                return File.ReadAllLines(filePath);
            }
        }

        internal bool FilesEqual(string sourcePath, string targetPath)
        {
            var resolvedTargetPath = ResolveFile(targetPath);

            return Helpers.FilesEqual(sourcePath, resolvedTargetPath);
        }

        private string ResolveFile(string filePath)
        {
            if (!UseVerifyOnlyForFile(filePath))
            {
                return filePath;
            }

            filePath = NormalizeFile(filePath);

            string? sourceFile = null, tempFile = null;

            if (RedirectedToTempl.TryGetValue(filePath, out tempFile))
            {
                // We'll get here if we're reading a file that was written to in WriteAllText
            }
            else
            {
                if (CopiedFilesToCheck.TryGetValue(filePath, out sourceFile))
                {
                    // We'll get here if we're reading a file that was copied

                    if (RedirectedToTempl.TryGetValue(sourceFile, out tempFile))
                    {
                        // We'll get here if we're reading a file that was copied and then written to in WriteAllText
                    }
                }
            }

            string targetFile = tempFile ?? sourceFile ?? filePath;
            return targetFile;
        }

        internal void DeleteFile(string targetFile, bool addVerifyErrorIfExists, out bool removed)
        {
            removed = false;
            bool verify = UseVerifyOnlyForFile(targetFile);

            if (verify)
            {
                removed = true;

                if (File.Exists(targetFile) && addVerifyErrorIfExists)
                {
                    VerifyErrors.Add($"Expected file {targetFile} to not exist");
                }
            }
            else
            {
                if (File.Exists(targetFile))
                {
                    removed = true;
                    File.Delete(targetFile);
                }
            }
        }

        internal void DeleteDirectoryRecursive(string path)
        {
            bool verify = UseVerifyOnlyForPath(path);

            if (verify)
            {
                if (Directory.Exists(path))
                {
                    VerifyErrors.Add($"Expected directory {path} to not exist");
                }
            }
            else
            {
                if (Directory.Exists(path))
                {
                    Directory.Delete(path, true);
                }
            }
        }

        private bool UseVerifyOnlyForFile(string file)
        {
            return UseVerifyOnlyInternal(file, true);
        }

        private bool UseVerifyOnlyForPath(string path)
        {
            return UseVerifyOnlyInternal(path, false);
        }

        private bool UseVerifyOnlyInternal(string path, bool trueForFile)
        {
            EnsureState();

            /*
            // if verifyOnly state
            if (verifyState)
            {
                return true;
            }*/

            // if !verifyOnly was passed to constructor, unconditional writes everywhere
            if (!verifyFromConstructor)
            {
                return false;
            }

            // if uncondo
            if (allowedUnconditionalPath is null)
            {
                return verifyState;
            }

            if (trueForFile ? IsSubFile(allowedUnconditionalPath, path) : IsSubPath(allowedUnconditionalPath, path))
            {
                return false;
            }
            else
            {
                return true;
            }
        }

        string? allowedUnconditionalPath;

        internal void StartUnconditionalWrites(string allowedUnconditionalPath)
        {
            EnsureState();

            if(verifyState != verifyFromConstructor)
            { 
                throw new Exception($"BUG: expected verifyState {verifyState} == verifyFromConstructor {verifyFromConstructor}");
            }

            if (!verifyFromConstructor)
            {
                return;
            }

            verifyState = false;
            this.allowedUnconditionalPath = allowedUnconditionalPath;
        }

        internal void ResumeWriteBehavior()
        {
            EnsureState();

            if (!verifyFromConstructor)
            {
                return;
            }

            verifyState = verifyFromConstructor;
            this.allowedUnconditionalPath = null;
        }

        private void EnsureState()
        {
            if(!verifyFromConstructor && verifyState)
            {
                throw new Exception("BUG: verifyState cannot be true if verifyFromConstructor is false");
            }

            if (verifyFromConstructor)
            {
                if (this.allowedUnconditionalPath is null && !verifyState)
                {
                    throw new Exception($"BUG: expected allowedUnconditionalPath={allowedUnconditionalPath} to be not null when !verifyState=={!verifyState}");
                }
            }
            else 
            {
                if (this.allowedUnconditionalPath is not null)
                {
                    throw new Exception($"BUG: expected allowedUnconditionalPath={allowedUnconditionalPath} to be null");
                }
            }
        }

        // generaetd by copilot 20240926
        static bool IsSubPath(string mainPath, string subPath)
        {
            var mainDirectory = new DirectoryInfo(mainPath).FullName;
            var subDirectory = new DirectoryInfo(subPath).FullName;

            return subDirectory.StartsWith(mainDirectory, StringComparison.OrdinalIgnoreCase);
        }

        static bool IsSubFile(string mainPath, string subFile)
        {
            var mainDirectory = new DirectoryInfo(mainPath).FullName;
            var subDirectory = new FileInfo(subFile).FullName;

            return subDirectory.StartsWith(mainDirectory, StringComparison.OrdinalIgnoreCase);
        }
    }
}